package moe.server;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import moe.config.Configuration;
import moe.entity.Post;
import moe.server.ImageCache.CacheType;

import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.ProtocolVersion;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;

public class CacheableResourcesHandler extends CollectionStaticResourcesHandler {
	private static final Logger log = Logger.getLogger(CacheableResourcesHandler.class.getName());

	private Pattern imagePattern = Pattern.compile("^.*/moe\\.imouto\\.p\\d+/.+$");
	private Pattern idPattern = Pattern.compile("^.*/moe (\\d+)[. ].+$");

	private ExecutorService threadPool = Executors.newCachedThreadPool(new ThreadFactory() {
		private int index = 0;

		public Thread newThread(Runnable r) {
			Thread thread = new Thread(r, "CacheUpdateThread_" + index++);
			thread.setDaemon(true);
			return thread;
		}
	});

	@Override
	public void handle(HttpRequest request, HttpResponse response, HttpContext context)
			throws HttpException, IOException {
		if (Configuration.getServerCache() < 1) {
			super.handle(request, response, context);
			return;
		}
		HttpSession session = getSession(context);
		if (session == null) {
			super.handle(request, response, context);
			return;
		}
		ImageCache cache = getCache(session);
		String id = getImageId(request);
		if (id == null || cache == null) {
			super.handle(request, response, context);
			return;
		}
		boolean bingo = false;
		byte[] data = null;
		if (cache.containsKey(id) && (data = cache.get(id)) != null && data.length > 0) {
			bingo = true;
			if (log.isLoggable(Level.FINER)) {
				log.finer("ID:" + id + " bingo cache");
				log.finer(request.getRequestLine() + " response from cache");
			}
			responseFromCache(request, response, data);
		} else {
			super.handle(request, response, context);
		}
		updateCache(cache, id, bingo);
	}

	private String getImageId(HttpRequest request) {
		String id = null, uri = request.getRequestLine().getUri();
		try {
			uri = URLDecoder.decode(uri, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			// ignore
		}
		if (imagePattern.matcher(uri).matches()) {
			Matcher matcher = idPattern.matcher(uri);
			if (matcher.find()) {
				id = matcher.group(1);
			}
		}
		return id;
	}

	private HttpSession getSession(HttpContext context) {
		return (HttpSession) context.getAttribute(HttpSession.SESSION_CONTEXT);
	}

	private void responseFromCache(HttpRequest request, HttpResponse response, byte[] bytes)
			throws IOException {
		File file = getFile(request);
		String lastModified = dateFormat.format(new Date(file.lastModified()));
		response.setStatusCode(HttpStatus.SC_OK);
		response.addHeader(HttpHeaders.LAST_MODIFIED, lastModified);
		String extension = file.getName().substring(file.getName().lastIndexOf('.') + 1);
		String mimeType = mimeTypeMap.get(extension);
		if (mimeType == null) {
			mimeType = "application/octet-stream";
		}
		ByteArrayEntity entity = new ByteArrayEntity(bytes);
		entity.setContentType(mimeType);
		response.setEntity(entity);
	}

	private File getFile(HttpRequest request) throws IOException {
		String requestURI = request.getRequestLine().getUri();
		int index = requestURI.indexOf("?");
		if (index != -1) {
			requestURI = requestURI.substring(0, index);
		}
		String filePath = resolveFilePath(URLDecoder.decode(requestURI, "UTF-8"));
		return new File(filePath);
	}

	@SuppressWarnings("unchecked")
	private ImageCache getCache(HttpSession session) {
		ImageCache cache = null;
		Object o = session.getAttribute(ImageCache.CACHE_CONTEXT);
		if (o != null) {
			cache = (ImageCache) o;
		}
		return cache;
	}

	private void updateCache(final ImageCache cache, final String id, final boolean bingo) {
		List<Post> posts = cache.getPostList();
		if (posts == null || posts.size() <= 1 || !hasPost(id, posts) || cache.getQuery() == null) {
			return;
		}
		threadPool.submit(new Runnable() {
			public void run() {
				synchronized (cache) {
					try {
						long startTime = System.currentTimeMillis();
						cache.setCurrentId(id);
						if (bingo) {
							cache.remove(cache.getCurrentId());
						}
						CacheType cacheType = resolveCacheType(cache, bingo);
						if (log.isLoggable(Level.FINER)) {
							log.finer("ID:" + id + ", CacheType: " + cacheType);
						}
						if (cacheType == null) {
							return;
						}
						doUpdateCache(cache, cacheType);
						cache.setCacheType(cacheType);
						cache.setTrackId(id);
						if (log.isLoggable(Level.FINER)) {
							log.finer("cache finish, used time:"
									+ (System.currentTimeMillis() - startTime) + "ms");
						}
					} catch (Throwable t) {
						t.printStackTrace();
						log.throwing(CacheableResourcesHandler.class.getName(), "updateCache", t);
					}
				}
			}
		});
	}

	private void doUpdateCache(ImageCache cache, CacheType cacheType) {
		List<Post> cacheablePosts = getCacheablePosts(cache, cacheType);
		if (log.isLoggable(Level.INFO)) {
			StringBuilder ids = new StringBuilder();
			for (Post post : cacheablePosts) {
				ids.append(post.id).append(" ");
			}
			if (log.isLoggable(Level.FINER)) {
				log.finer("cacheable posts:" + ids);
			}
		}
		doUpdateCache(cache, cacheablePosts);
	}

	private void doUpdateCache(ImageCache cache, List<Post> cacheablePosts) {
		// load data
		int length = cacheablePosts.size();
		byte[][] datas = new byte[length][];
		// load from old cache
		for (int i = 0; i < length; i++) {
			String id = String.valueOf(cacheablePosts.get(i).id);
			if (cache.containsKey(id)) {
				if (log.isLoggable(Level.FINER)) {
					log.finer("load data from old cache, id:" + id);
				}
				datas[i] = cache.get(id);
			}
		}
		cache.clear();
		// load from file
		for (int i = 0; i < length; i++) {
			Post post = cacheablePosts.get(i);
			if (datas[i] == null || datas[i].length == 0) {
				if (log.isLoggable(Level.FINER)) {
					log.finer("load data from file, id:" + post.id);
				}
				datas[i] = loadData(post);
			}
			if (datas[i] != null && datas[i].length == post.fileSize) {
				String id = String.valueOf(post.id);
				cache.put(id, datas[i]);
				if (log.isLoggable(Level.FINER)) {
					log.finer("cached id:" + id);
				}
			}
		}
	}

	private List<Post> getCacheablePosts(ImageCache cache, CacheType cacheType) {
		String currentId = cache.getCurrentId();
		List<Post> oldPosts = cache.getPostList();

		int index = getIndexFromPostList(currentId, oldPosts);
		int cacheSize = Configuration.getServerCache();

		List<Post> cacheablePosts = null;
		if (CacheType.UNKNOW == cacheType) {
			cacheablePosts = new ArrayList<Post>(2);
			if (index == 0) {
				cacheablePosts.add(oldPosts.get(index + 1));
				cacheablePosts.addAll(getPostsFromCrossPage(cache, 1, CacheType.BACKWARD));
			} else if (index == oldPosts.size() - 1) {
				cacheablePosts.add(oldPosts.get(index - 1));
				cacheablePosts.addAll(getPostsFromCrossPage(cache, 1, CacheType.FORWARD));
			} else {
				cacheablePosts.add(oldPosts.get(index - 1));
				cacheablePosts.add(oldPosts.get(index + 1));
			}
		} else {
			int remain = 0, fromIndex = 0, toIndex = 0;
			if (CacheType.FORWARD == cacheType) {
				remain = oldPosts.size() - index - 1;
				fromIndex = index + 1;
				if (remain < cacheSize) {
					toIndex = oldPosts.size();
				} else {
					toIndex = fromIndex + cacheSize;
				}
			} else if (CacheType.BACKWARD == cacheType) {
				remain = index;
				toIndex = index;
				if (remain < cacheSize) {
					fromIndex = 0;
				} else {
					fromIndex = toIndex - cacheSize;
				}
			}
			if (remain < cacheSize) {
				cacheablePosts = new ArrayList<Post>(cacheSize);
				cacheablePosts.addAll(oldPosts.subList(fromIndex, toIndex));
				cacheablePosts.addAll(getPostsFromCrossPage(cache, cacheSize - remain, cacheType));
			} else {
				cacheablePosts = oldPosts.subList(fromIndex, toIndex);
			}
		}
		return cacheablePosts;
	}

	private byte[] loadData(Post post) {
		HttpRequest request = new BasicHttpRequest("GET", post.fileUrl);
		HttpResponse response =
				new BasicHttpResponse(
						new ProtocolVersion("HTTP", 1, 1),
						HttpStatus.SC_BAD_REQUEST,
						"");
		HttpContext context = new BasicHttpContext();
		// StaticResourcesHandler handler = new StaticResourcesHandler();
		try {
			super.handle(request, response, context);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
		if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
			return null;
		}
		ByteArrayOutputStream baos = new ByteArrayOutputStream();
		HttpEntity entity = response.getEntity();
		if (entity == null) {
			return null;
		}
		byte[] result = null;
		try {
			entity.writeTo(baos);
			result = baos.toByteArray();
			baos.close();
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
		return result;
	}

	private List<Post> getPostsFromCrossPage(ImageCache cache, int gap, CacheType cacheType) {
		String[] page = cache.getQuery().get("page");
		if (!setCrossPage(cache, cacheType)) {
			return Collections.emptyList();
		}
		List<Post> posts = getPostFromQuery(cache);
		if (page == null) {
			cache.getQuery().remove("page");
		} else {
			cache.getQuery().put("page", page);
		}
		if (posts == null || posts.isEmpty()) {
			return Collections.emptyList();
		}
		if (posts.size() < gap) {
			gap = posts.size();
		}
		int fromIndex = 0, toIndex = 0;
		if (CacheType.FORWARD == cacheType) {
			toIndex = gap;
		} else {
			fromIndex = posts.size() - gap;
			toIndex = posts.size();
		}
		return posts.subList(fromIndex, toIndex);
	}

	private boolean setCrossPage(ImageCache cache, CacheType cacheType) {
		Map<String, String[]> query = cache.getQuery();
		if (query.containsKey("page")) {
			int page = Integer.valueOf(query.get("page")[0]);
			if (CacheType.FORWARD == cacheType) {
				query.put("page", new String[] { String.valueOf(page + 1) });
				return true;
			} else if (CacheType.BACKWARD == cacheType && page > 1) {
				query.put("page", new String[] { String.valueOf(page - 1) });
				return true;
			}
		}
		if (CacheType.FORWARD == cacheType) {
			query.put("page", new String[] { "2" });
			return true;
		}
		return false;
	}

	private List<Post> getPostFromQuery(ImageCache cache) {
		Map<String, String[]> query = cache.getQuery();
		// create mock object
		ImageCache tempCache = new ImageCache();
		HttpSession session = new HttpSession(null, 0);
		session.setAttribute(ImageCache.CACHE_CONTEXT, tempCache);

		HttpRequest request = new BasicHttpRequest("GET", "");
		HttpResponse response =
				new BasicHttpResponse(
						new ProtocolVersion("HTTP", 1, 1),
						HttpStatus.SC_BAD_REQUEST,
						"");
		HttpContext context = new BasicHttpContext();
		context.setAttribute(ExecutionContext.HTTP_REQUEST, request);
		context.setAttribute(ExecutionContext.HTTP_RESPONSE, response);
		context.setAttribute(HttpSession.SESSION_CONTEXT, session);

		JsonHandler handler = new JsonHandler();
		// mock query
		try {
			Method method =
					JsonHandler.class.getDeclaredMethod(
							"resolveAction",
							HttpContext.class,
							Map.class);
			method.setAccessible(true);
			method.invoke(handler, context, query);
		} catch (Exception e) {
			log.warning("Exception:" + e.getClass().getName() + " msg:" + e.getMessage());
			return null;
		}
		// take post
		return tempCache.getPostList();
	}

	private CacheType resolveCacheType(ImageCache cache, boolean bingo) {
		CacheType cacheType = cache.getCacheType();
		if (bingo) {
			if (cacheType == CacheType.UNKNOW) {
				cacheType = resolveCacheType(cache);
			}
		} else {
			cacheType = resolveCacheType(cache);
		}
		return cacheType;
	}

	private CacheType resolveCacheType(ImageCache cache) {
		String trackId = cache.getTrackId(), currentId = cache.getCurrentId();
		if (log.isLoggable(Level.FINER)) {
			log.finer("Cache trackId:" + trackId + ", currentId:" + currentId);
		}
		List<Post> posts = cache.getPostList();
		CacheType cacheType = null;
		if (trackId != null && hasPost(trackId, posts)) {
			if (compareId(trackId, currentId, posts) < 0) {// trackId before currentId
				cacheType = CacheType.FORWARD;
			} else if (compareId(trackId, currentId, posts) > 0) {// trackId after currentId
				cacheType = CacheType.BACKWARD;
			} else {// trackId equal currentId
				// not update cache
				cacheType = null;
			}
		} else {
			// cache before and after
			cacheType = CacheType.UNKNOW;
		}
		return cacheType;
	}

	private boolean hasPost(String id, List<Post> posts) {
		return getIndexFromPostList(id, posts) != -1;
	}

	private int getIndexFromPostList(String id, List<Post> posts) {
		Post post = getPostById(id, posts);
		return posts.indexOf(post);
	}

	private Post getPostById(String id, List<Post> posts) {
		for (Post post : posts) {
			if (id.equals(String.valueOf(post.id))) {
				return post;
			}
		}
		return null;
	}

	private int compareId(String trackId, String currentId, List<Post> posts) {
		int trackIndex = getIndexFromPostList(trackId, posts);
		if (trackIndex == -1) {
			throw new RuntimeException("trackId not in postList, trackId:" + trackId);
		}
		int currentIndex = getIndexFromPostList(currentId, posts);
		if (currentIndex == -1) {
			throw new RuntimeException("currentId not in postList, currentId:" + currentId);
		}
		return trackIndex - currentIndex;
	}
}
